﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using Xceed.Silverlight.Data;

namespace Xceed.Silverlight.DataGrid.Export
{
  public abstract class ExporterBase
  {
    protected ExporterBase( DataGridControl dataGridControl )
    {
      if( dataGridControl == null )
        throw new ArgumentNullException( "dataGridControl" );


      m_dataGridControl = dataGridControl;
      this.IncludeColumnHeaders = true;
      //By default, Column.Title is used.
      this.UseFieldNamesInHeaders = false;
    }

    #region IncludeColumnHeaders Property

    public bool IncludeColumnHeaders
    {
      get;
      set;
    }

    #endregion

    #region UseFieldNamesInHeaders Property

    public bool UseFieldNamesInHeaders
    {
      get;
      set;
    }

    #endregion

    #region BaseStream Property

    protected Stream BaseStream
    {
      get
      {
        return m_stream;
      }
    }

    #endregion

    public void Export( Stream stream, IList<SelectionRange> selectionRanges )
    {
      if( m_dataGridControl.DataGridContext.DataSource.Provider.IsAsynchronous )
        throw new InvalidOperationException( "An attempt was made to start a synchronous operation using an asynchronous data source." );

      if( ExporterBase.IsExporting )
        throw new InvalidOperationException( "An attempt was made to start an export operation while one is already in progress." );

      if( stream == null )
        throw new ArgumentNullException( "stream" );

      m_isSynchronous = true;
      ExporterBase.IsExporting = true;
      m_cancelOperation = false;

      try
      {
        m_stream = stream;

        m_groupTitles.Clear();
        m_rootContext = new ExportContext();

        this.FillExportColumnList();

        //Setup the exported file.
        this.StartExport();

        if( this.IncludeColumnHeaders )
          this.ExportColumnHeaders( m_rootContext );

        //Setup the selection range and the enumerator.
        this.SetRangeAndEnumerator( selectionRanges );

        //Start the actual data exporting.
        this.DoExportData();
      }
      finally
      {
        //Make sure to release resources.
        this.CleanStream();
        m_isSynchronous = false;
      }
    }

    public IAsyncResult BeginExport( Stream stream, IList<SelectionRange> selectionRanges, AsyncCallback asyncCallback, object asyncState )
    {
      if( ExporterBase.IsExporting )
        throw new InvalidOperationException( "An attempt was made to start an export operation while one is already in progress." );

      if( stream == null )
        throw new ArgumentNullException( "stream" );

      if( asyncCallback == null )
        throw new ArgumentNullException( "asyncCallback" );

      ExporterBase.IsExporting = true;
      m_cancelOperation = false;
      m_asyncResult = new ExporterAsyncResult( asyncCallback, asyncState );

      try
      {
        m_stream = stream;

        m_groupTitles.Clear();
        m_rootContext = new ExportContext();

        this.FillExportColumnList();

        //Setup the exported file.
        this.StartExport();

        if( this.IncludeColumnHeaders )
          this.ExportColumnHeaders( m_rootContext );

        //Setup the selection range and the enumerator.
        this.SetRangeAndEnumerator( selectionRanges );

        //Start the actual data exporting.
        this.DoExportData();
      }
      catch( Exception )
      {
        //Make sure to release resources.
        this.CleanStream();
        throw;
      }

      return m_asyncResult;
    }

    public Stream EndExport( IAsyncResult asyncResult )
    {
      if( asyncResult == null )
        throw new ArgumentException( "asyncResult" );

      ExporterAsyncResult exporterAsyncResult = asyncResult as ExporterAsyncResult;

      if( exporterAsyncResult == null )
        throw new ArgumentException( "The specified asyncResult must be an ExporterAsyncResult.", "asyncResult" );

      if( exporterAsyncResult != m_asyncResult )
        throw new ArgumentException( "An invalid IAsyncResult was provided.", "asyncResult" );

      if( !asyncResult.IsCompleted )
        throw new InvalidOperationException( "The asynchronous operation must be completed before calling the EndExport method." );

      //Release resources
      Stream stream = this.CleanStream();
      return stream;
    }

    public void CancelExport()
    {
      if( ExporterBase.IsExporting )
      {
        m_cancelOperation = true;

        if( m_dataGridControl.DataSource.Provider.IsAsynchronous )
        {
          m_dataRangeEnumerator.Dispose();
          this.FinishExporting();
        }
      }
    }

    protected virtual void StartExport()
    {
    }

    protected virtual void StopExport()
    {
    }

    protected virtual void StartHeader( ExportContext exportContext, IList<ExportColumn> columns )
    {
    }
    protected virtual void StopHeader( ExportContext exportContext, IList<ExportColumn> columns )
    {
    }

    protected virtual void StartHeaderField( ExportContext exportContext, string headerValue )
    {
    }
    protected virtual void StopHeaderField( ExportContext exportContext, string headerValue )
    {
    }

    protected virtual void StartGroup( string groupTitle, string groupValue )
    {
    }
    protected virtual void StopGroup( string groupTitle, string groupValue )
    {
    }

    protected virtual void StartDataItem( ExportContext exportContext, object dataItem )
    {
    }
    protected virtual void StopDataItem( ExportContext exportContext, object dataItem )
    {
    }

    protected virtual void StartDataItemField( ExportContext exportContext, object fieldValue )
    {
    }
    protected virtual void StopDataItemField( ExportContext exportContext, object fieldValue )
    {
    }

    private void FillExportColumnList()
    {
      foreach( Column column in m_dataGridControl.Columns )
      {
        if( column.Visible )
          m_rootContext.Columns.AddAccordingToVisiblePosition( column );
      }
    }

    private void ExportColumnHeaders( ExportContext exportContext )
    {
      string header;

      this.StartHeader( exportContext, exportContext.Columns );

      exportContext.ResetColumnEnumerator();
      while( exportContext.MoveNextColumn() )
      {
        header = this.GetHeaderString( exportContext.CurrentColumn.FieldName, exportContext.CurrentColumn.Title );
        this.StartHeaderField( exportContext, header );
        this.StopHeaderField( exportContext, header );
      }

      this.StopHeader( exportContext, exportContext.Columns );
    }

    private void SetRangeAndEnumerator( IList<SelectionRange> selectionRanges )
    {
      DataSourceWrapper dataSource = m_dataGridControl.DataGridContext.DataSource;

      List<DataRange> dataRanges = new List<DataRange>( 1 );

      //If no range is provided.
      if( selectionRanges == null )
      {
        //Set it to the whole data in the source.
        dataRanges.Add( DataRange.All( true, dataSource.GetCurrentDataRangeContext() ) );
      }
      else
      {
        foreach( SelectionRange selectionRange in selectionRanges )
        {
          //If there is no range context, set one.
          if( selectionRange.DataRangeContext == null )
          {
            selectionRange.DataRangeContext = SelectionRangeHelper.BuildDataRangeContext( dataSource, selectionRange );
          }

          //Then convert the SelectionRange to a DataRange.
          dataRanges.Add( SelectionRangeHelper.ConvertSelectionRange( selectionRange ) );
        }
      }

      //Get and set the enumerator.
      m_dataRangeEnumerator = dataSource.GetDataRangeEnumerator( dataRanges, dataSource.GroupDescriptions, dataSource.SortDescriptions );
      m_dataRangeEnumerator.Reset();
    }

    private void DoExportData()
    {
      DataGridContext dataGridContext = m_dataGridControl.DataGridContext;

      m_grouped = false;
      ObservableCollection<GroupDescription> groupDescriptions = dataGridContext.GroupDescriptions;
      //Used to build a dummy DataPath for the first time a later loop.
      DataItem[] dataItems;

      //If the grid is grouped.
      if( groupDescriptions.Count > 0 )
      {
        m_grouped = true;
        dataItems = new DataItem[ groupDescriptions.Count ];

        //Get a reference to the title of each group level.
        foreach( GroupDescription description in groupDescriptions )
        {
          dataItems[ groupDescriptions.IndexOf( description ) ] = new DataItem();
          string groupLevelFieldName = description.GetPropertyName();
          string groupTitle = groupLevelFieldName;

          foreach( ExportColumn column in m_rootContext.Columns )
          {
            if( column.FieldName == groupLevelFieldName )
            {
              groupTitle = this.GetHeaderString( column.FieldName, column.Title );
              break;
            }
          }
          m_groupTitles.Add( groupTitle );
        }
      }
      else
      {
        dataItems = new DataItem[ 1 ] { new DataItem() };
      }

      //Initialize the previous parent path with a dummy DataPath.
      m_previousParentPath = new DataPath( dataItems );

      //Start enumerating items.
      this.MoveRangeEnumerator();
    }

    private void MoveRangeEnumerator()
    {
      bool moveNextSucceeded = true;
      bool completedSynchronously = true;
      bool isSynchronous = m_isSynchronous;
      IAsyncResult asyncResult = null;
      TimeSpan span;
      DateTime start = DateTime.Now;

      try
      {
        //This loop will exit every 150 milisecondes (ExporterBase.MaxSpan in fact) if the range enumerator is running synchronously.
        do
        {
          asyncResult = m_dataRangeEnumerator.BeginGetNext( new AsyncCallback( BeginGetNextCallback ), null );
          if( asyncResult.CompletedSynchronously )
          {
            GetNextResult result = m_dataRangeEnumerator.EndGetNext( asyncResult );
            if( result.Success && !m_cancelOperation )
            {
              //Start the actual exporting of the item
              this.ExportPath( result.Item );
            }
            else
            {
              //Stop the loop
              moveNextSucceeded = false;
              isSynchronous = false;
              this.FinishExporting();
            }
          }
          else
          {
            completedSynchronously = false;
          }
          span = DateTime.Now - start;
        }
        while( isSynchronous || ( span < ExporterBase.MaxSpan && moveNextSucceeded && completedSynchronously ) );

        //If the enumerator is running synchronously.
        if( moveNextSucceeded && completedSynchronously )
        {
          //dispatach the next loop so the UI will be responsive.
          m_dataGridControl.Dispatcher.BeginInvoke( new Action( MoveRangeEnumerator ) );
          m_asyncResult.CompletedSynchronously = false;
        }
      }
      catch( Exception )
      {
        this.CleanStream();
        throw;
      }

      if( !completedSynchronously )
      {
        m_asyncResult.CompletedSynchronously = false;
      }
    }

    private void BeginGetNextCallback( IAsyncResult asyncResult )
    {
      if( asyncResult.CompletedSynchronously || m_cancelOperation )
        return;

      try
      {
        GetNextResult result = m_dataRangeEnumerator.EndGetNext( asyncResult );
        if( result.Success )
        {
          this.ExportPath( result.Item );
          //Continue the loop.  No need to dispatch here since there will never be more then one call stacked.
          this.MoveRangeEnumerator();
        }
        else
        {
          this.FinishExporting();
        }
      }
      catch( Exception )
      {
        this.CleanStream();
        throw;
      }
    }

    private void ExportPath( DataPath path )
    {
      if( path != null )
      {
        //If grouped, verify if this path is the beginning of a new group.
        if( m_grouped && !object.Equals( path.ParentPath, m_previousParentPath ) )
        {
          for( int level = 0; level < path.Depth - 1; level++ )
          {
            if( !path[ level ].Equals( m_previousParentPath[ level ] ) )
            {
              //If so, export the group title.
              string groupValue = path[ level ].Data != null ? path[ level ].Data.ToString() : string.Empty;
              this.StartGroup( m_groupTitles[ level ], groupValue );
              this.StopGroup( m_groupTitles[ level ], groupValue );
            }
          }
        }
        this.ExportDataItem( m_rootContext, path.LastChild.Data );
        m_previousParentPath = path.ParentPath;
      }
    }

    private void ExportDataItem( ExportContext exportContext, object dataItem )
    {
      //Start the DataItem row
      this.StartDataItem( exportContext, dataItem );
      //Export each field
      this.ExportDataItemFields( exportContext, dataItem );
      //End the DataItem row
      this.StopDataItem( exportContext, dataItem );
    }

    private void ExportDataItemFields( ExportContext exportContext, object dataItem )
    {
      object fieldValue = null;
      ExportColumn currentColumn;
      BindingPathValueExtractor valueExtractor = null;
      bool isContextSet = false;

      List<PropertyInfo> propertyInfos = new List<PropertyInfo>( dataItem.GetType().GetProperties() );

      exportContext.ResetColumnEnumerator();
      while( exportContext.MoveNextColumn() )
      {
        currentColumn = exportContext.CurrentColumn;
        fieldValue = null;

        //Get the property that is used for binding for the current column
        string propertyName = currentColumn.SourceColumn.DisplayMemberBindingInfo.Path.Path;

        //If there is '[', '{', or '.' in the property path, we must use the framework binding to be able to extract the value.
        if( this.IsComplexProperty( propertyName ) )
        {
          if( !isContextSet )
          {
            isContextSet = true;
            //Used as a binding target, so the cell value can be correctly extracted from the dataItem.            
            valueExtractor = new BindingPathValueExtractor();
            valueExtractor.DataContext = dataItem;
          }

          valueExtractor.SetBinding( BindingPathValueExtractor.ContentProperty, currentColumn.SourceColumn.DisplayMemberBinding );
          fieldValue = valueExtractor.Content;

          //Undo the binding, so if the same object is constantly used, performance will not degrade due to bindings piling up.
          valueExtractor.ClearValue( BindingPathValueExtractor.ContentProperty );
        }
        //If this is a simple property path, simply use reflection to extract the value, which is much more performant than binding.
        else
        {
          int index = 0;
          for( int i = 0; i < propertyInfos.Count; i++ )
          {
            if( propertyInfos[ i ].Name == propertyName )
            {
              index = i;
              break;
            }
          }
          fieldValue = propertyInfos[ index ].GetValue( dataItem, null );
          propertyInfos.RemoveAt( index );
        }

        //Export the cell value
        this.StartDataItemField( exportContext, fieldValue );
        this.StopDataItemField( exportContext, fieldValue );
      }
    }

    private string GetHeaderString( string fieldName, object title )
    {
      string header;

      //Verify if title can be used instead of fieldname.
      if( ( title == null ) || ( this.UseFieldNamesInHeaders ) )
      {
        header = fieldName;
      }
      else
      {
        header = title.ToString();
      }

      if( header == null )
        header = "";

      return header;
    }

    private bool IsComplexProperty( string propertyName )
    {
      if( propertyName.IndexOfAny( chars ) != -1 )
      {
        return true;
      }

      return false;
    }

    private void FinishExporting()
    {
      this.StopExport();
      ExporterBase.IsExporting = false;
      if( !m_isSynchronous )
      {
        m_asyncResult.IsCompleted = true;
        m_asyncResult.AsyncCallback.Invoke( m_asyncResult );
      }
    }

    private Stream CleanStream()
    {
      //Release the reference to the stream, so the caller can dispose it properly.
      Stream stream = m_stream;
      m_stream = null;
      if( stream != null )
      {
        stream.Flush();
      }
      return stream;
    }

    private Stream m_stream;
    private DataGridControl m_dataGridControl;
    private ExportContext m_rootContext;
    private List<string> m_groupTitles = new List<string>();
    private DataPath m_previousParentPath;
    private IDataRangeEnumerator m_dataRangeEnumerator;
    private ExporterAsyncResult m_asyncResult;
    private bool m_grouped;
    private bool m_cancelOperation;
    private bool m_isSynchronous = false;

    private static bool IsExporting = false;

    private static readonly TimeSpan MaxSpan = TimeSpan.FromMilliseconds( 150 );
    private static readonly char[] chars = new char[ 3 ] { '[', '{', '.' };
  }
}
